# -*- coding: utf-8 -*-
# @Time    : 2024/7/2 11:26
# @Author  : yujiahao
# @File    : 35_thread_lock.py
# @description:多线程中锁的问题


# todo 案例一 100个线程同时操作一个数据（有问题）

'''
因为多个线程可以共享进程的内存空间，因此要实现多个线程间的通信相对简单，大家能想到的最直接的办法就是设置一个全局变量，多个线程共享这个全局变量即可。
但是当多个线程共享同一个变量（我们通常称之为“资源”）的时候，很有可能产生不可控的结果从而导致程序失效甚至崩溃。如果一个资源被多个线程竞争使用，
那么我们通常称之为“临界资源”，对“临界资源”的访问需要加上保护，否则资源会处于“混乱”的状态。
下面的例子演示了100个线程向同一个银行账户转账（转入1元钱）的场景，在这个例子中，银行账户就是一个临界资源，
在没有保护的情况下我们很有可能会得到错误的结果。
'''

from threading import Thread, Lock
from time import time, sleep


class Account(object):
    '''
    账户类，用来模拟存钱业务
    '''

    def __init__(self):
        self._balance = 0

    def deposit(self, money):
        # 计算存款后的余额
        new_balance = self._balance + money
        # 模拟受理存款业务需要0.01秒的时间
        sleep(0.01)
        # 修改账户余额
        self._balance = new_balance

    # @property 装饰器将一个方法转换为只读属性。可以像访问属性一样访问方法的返回值，而不需要使用括号。
    @property
    def balance(self):
        return self._balance


class AddMoneyThread(Thread):
    '''模拟存钱的类，并继承线程类'''

    def __init__(self, account, money):
        super().__init__()
        self._account = account
        self._money = money

    def run(self):
        self._account.deposit(self._money)


def add_money():
    account = Account()
    threads = []
    # 创建100个存款的线程向同一个账户中存钱
    for _ in range(100):
        t = AddMoneyThread(account, 1)
        threads.append(t)
        t.start()
    # 等所有存款的线程都执行完毕
    for t in threads:
        t.join()
    print('账户余额为: ￥%d元' % account.balance)


# todo 纠正错误，加锁
class Account_2(object):
    '''
    运行上面的程序，结果让人大跌眼镜，100个线程分别向账户中转入1元钱，结果居然远远小于100元。
    之所以出现这种情况是因为我们没有对银行账户这个“临界资源”加以保护，多个线程同时向账户中存钱时，
    会一起执行到new_balance = self._balance + money这行代码，多个线程得到的账户余额都是初始状态下的0，
    所以都是0上面做了+1的操作，因此得到了错误的结果。在这种情况下，“锁”就可以派上用场了。
    我们可以通过“锁”来保护“临界资源”，只有获得“锁”的线程才能访问“临界资源”，而其他没有得到“锁”的线程只能被阻塞起来，
    直到获得“锁”的线程释放了“锁”，其他线程才有机会获得“锁”，进而访问被保护的“临界资源”。
    '''

    def __init__(self):
        self._balance = 0
        # 锁的导入方式
        # self.lock = threading.Lock() 明确表明 Lock 来自 threading 模块，代码更具可读性。
        # self._lock = Lock() 省略了模块前缀，前提是已经从 threading 模块中导入了 Lock 类
        self._lock = Lock()

    # ① 手动获取锁

    # def deposit(self, money):
    #     # 先获取锁才能执行后续的代码
    #     self._lock.acquire()
    #     try:
    #         new_balance = self._balance + money
    #         sleep(0.01)
    #         self._balance = new_balance
    #     finally:
    #         # 在finally中执行释放锁的操作保证正常异常锁都能释放
    #         self._lock.release()

    # ② 使用with自动获取和关闭锁
    def deposit(self, money):
        with self._lock:
            new_balance = self._balance + money
            sleep(0.01)
            self._balance = new_balance

    @property
    def balance(self):
        return self._balance


def add_money_2():
    account = Account_2()
    threads = []
    for _ in range(100):
        t = AddMoneyThread(account, 1)
        threads.append(t)
        t.start()
    for t in threads:
        t.join()
    print('账户余额为: ￥%d元' % account.balance)


def main():
    print('\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n')
    # 调用方法，发现结果不对，根本就不足100元，只有1元，其实是没有加锁
    add_money()

    '''
    比较遗憾的一件事情是Python的多线程并不能发挥CPU的多核特性，这一点只要启动几个执行死循环的线程就可以得到证实了。
    之所以如此，是因为Python的解释器有一个“全局解释器锁”（GIL）的东西，任何线程执行前必须先获得GIL锁，然后每执行100条字节码，
    解释器就自动释放GIL锁，让别的线程有机会执行，这是一个历史遗留问题，但是即便如此，就如我们之前举的例子，使用多线程在提升执行
    效率和改善用户体验方面仍然是有积极意义的。
    '''
    add_money_2()


if __name__ == '__main__':
    main()
